create-project: Add --branch and cleanup arguments The new --branch option can be used to create the initial branch with a name other than the Git default of master. I also cleaned up the command line parser used by create-branch so most of the heavy work is done by args4j, rather than within the application code. Bug: GERRIT-280 Change-Id: I216f5291095bf528e6e92de3550101e2d41ea4df Signed-off-by: Shawn O. Pearce <sop@google.com> 
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt index 08fbd5c..d1e08b1 100644 --- a/Documentation/cmd-create-project.txt +++ b/Documentation/cmd-create-project.txt 
@@ -10,11 +10,12 @@  [verse]  'ssh' -p <port> <host> 'gerrit create-project' \  \--name <NAME> \ -[\--owner <OWNER>] \ +[--branch <REF>] \ +[\--owner <GROUP>] \  [\--description <DESC>] \  [\--submit-type <TYPE>] \ -[\--use-contributor-agreements {true|false}] \ -[\--use-signed-off-by {true|false}] +[\--use-contributor-agreements] \ +[\--use-signed-off-by]    DESCRIPTION  ----------- @@ -43,6 +44,10 @@ 	Required; name of the project to create. If name ends with 	`.git` the suffix will be automatically removed.   +\--branch:: +	Name of the initial branch in the newly created project. +	Defaults to 'master'. +  \--owner:: 	Name of the group which will initially own this repository. 	The specified group must already be defined within Gerrit. @@ -64,13 +69,13 @@ 	Action used by Gerrit to submit an approved change to its 	destination branch. Supported options are:  + -* fast-forward-only: produces a strictly linear history. -* merge-if-necessary: create a merge commit when required. -* merge-always: always create a merge commit. -* cherry-pick: always cherry-pick the commit. +* FAST_FORWARD_ONLY: produces a strictly linear history. +* MERGE_IF_NECESSARY: create a merge commit when required. +* MERGE_ALWAYS: always create a merge commit. +* CHERRY_PICK: always cherry-pick the commit.    + -Defaults to fast-forward-only. For more details see +Defaults to MERGE_IF_NECESSARY. For more details see  link:project-setup.html#submit_type[Change Submit Actions].    \--use-contributor-agreements:: @@ -108,13 +113,13 @@  The remote repository creation is performed by a Bourne shell script:    ==== - mkdir -p '/base/project.git' && cd '/base/project.git' && git init --bare + mkdir -p '/base/project.git' && cd '/base/project.git' && git init --bare && git update-ref HEAD refs/heads/master  ====   -For this to work successfully the remote system must be able to -run arbitrary shell scripts, and must have `git` in the user's PATH -environment variable. Administrators can run this command by hand -to establish a new empty repository if necessary. +For this to work successfully the remote system must be able to run +arbitrary shell scripts, and must have `git` in the user's PATH +environment variable. Administrators could also run this command line +by hand to establish a new empty repository.    SEE ALSO  -------- 
diff --git a/src/main/java/com/google/gerrit/git/PushReplication.java b/src/main/java/com/google/gerrit/git/PushReplication.java index d754e14..77752be 100644 --- a/src/main/java/com/google/gerrit/git/PushReplication.java +++ b/src/main/java/com/google/gerrit/git/PushReplication.java 
@@ -174,7 +174,7 @@  return result;  }   - public void replicateNewProject(Project.NameKey projectName) { + public void replicateNewProject(Project.NameKey projectName, String head) {  if (!isEnabled()) {  return;  } @@ -188,13 +188,12 @@  Iterator<URIish> uriIter = uriList.iterator();    while (uriIter.hasNext()) { - replicateProject(uriIter.next()); + replicateProject(uriIter.next(), head);  }  }  }   - - private void replicateProject(final URIish replicateURI) { + private void replicateProject(final URIish replicateURI, final String head) {  SshSessionFactory sshFactory = SshSessionFactory.getInstance();  Session sshSession;  String projectPath = QuotedString.BOURNE.quote(replicateURI.getPath()); @@ -208,7 +207,8 @@  OutputStream errStream = createErrStream();  String cmd = "mkdir -p " + projectPath  + "&& cd " + projectPath - + "&& git init --bare"; + + "&& git init --bare" + + "&& git symbolic-ref HEAD " + QuotedString.BOURNE.quote(head);    try {  sshSession = sshFactory.getSession(replicateURI.getUser(), 
diff --git a/src/main/java/com/google/gerrit/git/ReplicationQueue.java b/src/main/java/com/google/gerrit/git/ReplicationQueue.java index b9abd8a..2d71301 100644 --- a/src/main/java/com/google/gerrit/git/ReplicationQueue.java +++ b/src/main/java/com/google/gerrit/git/ReplicationQueue.java 
@@ -53,6 +53,7 @@  * that the project will be created at the remote sites as well.  *  * @param project of the project to be created. + * @param head name HEAD should point at (must be {@code refs/heads/...}).  */ - void replicateNewProject(Project.NameKey project); + void replicateNewProject(Project.NameKey project, String head);  } 
diff --git a/src/main/java/com/google/gerrit/server/ssh/SshModule.java b/src/main/java/com/google/gerrit/server/ssh/SshModule.java index e98d1a7..e04486f 100644 --- a/src/main/java/com/google/gerrit/server/ssh/SshModule.java +++ b/src/main/java/com/google/gerrit/server/ssh/SshModule.java 
@@ -17,6 +17,7 @@  import static com.google.inject.Scopes.SINGLETON;    import com.google.gerrit.client.reviewdb.Account; +import com.google.gerrit.client.reviewdb.AccountGroup;  import com.google.gerrit.client.reviewdb.PatchSet;  import com.google.gerrit.pgm.CmdLineParser;  import com.google.gerrit.pgm.OptionHandlerFactory; @@ -27,6 +28,7 @@  import com.google.gerrit.server.config.FactoryModule;  import com.google.gerrit.server.config.GerritRequestModule;  import com.google.gerrit.server.project.ProjectControl; +import com.google.gerrit.server.ssh.args4j.AccountGroupIdHandler;  import com.google.gerrit.server.ssh.args4j.AccountIdHandler;  import com.google.gerrit.server.ssh.args4j.PatchSetIdHandler;  import com.google.gerrit.server.ssh.args4j.ProjectControlHandler; @@ -104,6 +106,7 @@  factory(CmdLineParser.Factory.class);    registerOptionHandler(Account.Id.class, AccountIdHandler.class); + registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);  registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);  registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);  } 
diff --git a/src/main/java/com/google/gerrit/server/ssh/args4j/AccountGroupIdHandler.java b/src/main/java/com/google/gerrit/server/ssh/args4j/AccountGroupIdHandler.java new file mode 100644 index 0000000..63be4a5 --- /dev/null +++ b/src/main/java/com/google/gerrit/server/ssh/args4j/AccountGroupIdHandler.java 
@@ -0,0 +1,57 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.ssh.args4j; + +import com.google.gerrit.client.reviewdb.AccountGroup; +import com.google.gerrit.server.account.GroupCache; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.OptionDef; +import org.kohsuke.args4j.spi.OptionHandler; +import org.kohsuke.args4j.spi.Parameters; +import org.kohsuke.args4j.spi.Setter; + +public class AccountGroupIdHandler extends OptionHandler<AccountGroup.Id> { + private final GroupCache groupCache; + + @SuppressWarnings("unchecked") + @Inject + public AccountGroupIdHandler(final GroupCache groupCache, + @Assisted final CmdLineParser parser, @Assisted final OptionDef option, + @Assisted final Setter setter) { + super(parser, option, setter); + this.groupCache = groupCache; + } + + @Override + public final int parseArguments(final Parameters params) + throws CmdLineException { + final String n = params.getParameter(0); + final AccountGroup group = groupCache.lookup(n); + if (group == null) { + throw new CmdLineException(owner, "Group \"" + n + "\" does not exist"); + } + setter.addValue(group.getId()); + return 1; + } + + @Override + public final String getDefaultMetaVariable() { + return "GROUP"; + } +} 
diff --git a/src/main/java/com/google/gerrit/server/ssh/commands/AdminCreateProject.java b/src/main/java/com/google/gerrit/server/ssh/commands/AdminCreateProject.java index 8249620..c527536 100644 --- a/src/main/java/com/google/gerrit/server/ssh/commands/AdminCreateProject.java +++ b/src/main/java/com/google/gerrit/server/ssh/commands/AdminCreateProject.java 
@@ -23,7 +23,6 @@  import com.google.gerrit.client.reviewdb.Project.SubmitType;  import com.google.gerrit.git.GitRepositoryManager;  import com.google.gerrit.git.ReplicationQueue; -import com.google.gerrit.server.account.GroupCache;  import com.google.gerrit.server.config.AuthConfig;  import com.google.gerrit.server.ssh.AdminCommand;  import com.google.gerrit.server.ssh.BaseCommand; @@ -32,6 +31,7 @@  import com.google.inject.Inject;    import org.kohsuke.args4j.Option; +import org.spearce.jgit.lib.Constants;  import org.spearce.jgit.lib.Repository;    import java.io.PrintWriter; @@ -40,23 +40,29 @@  /** Create a new project. **/  @AdminCommand  final class AdminCreateProject extends BaseCommand { - @Option(name = "--name", required = true, aliases = { "-n" }, usage = "name of project to be created") + @Option(name = "--name", required = true, aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created")  private String projectName;   - @Option(name = "--owner", aliases = { "-o" }, usage = "name of group that will own the project (defaults to: Administrators)") - private String ownerName; + @Option(name = "--owner", aliases = {"-o"}, usage = "owner of project\n" + + "(default: Administrators)") + private AccountGroup.Id ownerId;   - @Option(name = "--description", aliases = { "-d" }, usage = "description of the project") - private String projectDescription; + @Option(name = "--description", aliases = {"-d"}, metaVar = "DESC", usage = "description of project") + private String projectDescription = "";   - @Option(name = "--submit-type", aliases = { "-t" }, usage = "project submit type (F)ast forward only, (M)erge if necessary, merge (A)lways or (C)herry pick (defaults to: F)") - private String submitTypeStr; + @Option(name = "--submit-type", aliases = {"-t"}, usage = "project submit type\n" + + "(default: MERGE_IF_NECESSARY)") + private SubmitType submitType = SubmitType.MERGE_IF_NECESSARY;   - @Option(name = "--use-contributor-agreements", aliases = { "--ca" }, usage = "set this to true if project should make the user sign a contributor agreement (defaults to: N)") - private String useContributorAgreements; + @Option(name = "--use-contributor-agreements", aliases = {"--ca"}, usage = "if contributor agreement is required") + private boolean contributorAgreements;   - @Option(name = "--use-signed-off-by", aliases = { "--so" }, usage = "set this to true if the project should mandate signed-off-by (defaults to: N)") - private String useSignedOffBy; + @Option(name = "--use-signed-off-by", aliases = {"--so"}, usage = "if signed-off-by is required") + private boolean signedOffBy; + + @Option(name = "--branch", aliases = {"-b"}, metaVar = "BRANCH", usage = "initial branch name\n" + + "(default: master)") + private String branch = Constants.MASTER;    @Inject  private ReviewDb db; @@ -65,19 +71,11 @@  private GitRepositoryManager repoManager;    @Inject - private GroupCache groupCache; - - @Inject  private AuthConfig authConfig;    @Inject  private ReplicationQueue rq;   - private AccountGroup.Id ownerId = null; - private boolean contributorAgreements = false; - private boolean signedOffBy = false; - private SubmitType submitType = null; -  @Override  public void start() {  startThread(new CommandRunnable() { @@ -85,6 +83,7 @@  public void run() throws Exception {  PrintWriter p = toPrintWriter(out);   + ownerId = authConfig.getAdministratorsGroup();  parseCommandLine();    try { @@ -94,16 +93,17 @@    createProject(txn);   - Repository repo = repoManager.createRepository(projectName); + Repository repo = repoManager.createRepository(projectName);  repo.create(true); + repo.writeSymref(Constants.HEAD, branch);  repoManager.setProjectDescription(projectName, projectDescription);    txn.commit();   - rq.replicateNewProject(new Project.NameKey(projectName)); + rq.replicateNewProject(new Project.NameKey(projectName), branch);  } catch (Exception e) { - p.print("Error when trying to create project: " - + e.getMessage() + "\n"); + p.print("Error when trying to create project: " + e.getMessage() + + "\n");  p.flush();  }   @@ -112,12 +112,10 @@  }    private void createProject(Transaction txn) throws OrmException { - final Project.NameKey newProjectNameKey = - new Project.NameKey(projectName); + final Project.NameKey newProjectNameKey = new Project.NameKey(projectName);    final Project newProject = - new Project(newProjectNameKey, - new Project.Id(db.nextProjectId())); + new Project(newProjectNameKey, new Project.Id(db.nextProjectId()));    newProject.setDescription(projectDescription);  newProject.setSubmitType(submitType); @@ -127,85 +125,31 @@  db.projects().insert(Collections.singleton(newProject), txn);    final ProjectRight.Key prk = - new ProjectRight.Key(newProjectNameKey, - ApprovalCategory.OWN, ownerId); + new ProjectRight.Key(newProjectNameKey, ApprovalCategory.OWN, ownerId);  final ProjectRight pr = new ProjectRight(prk);  pr.setMaxValue((short) 1);  pr.setMinValue((short) 1);  db.projectRights().insert(Collections.singleton(pr), txn);    final Branch newBranch = - new Branch( - new Branch.NameKey(newProjectNameKey, Branch.R_HEADS + "master")); - + new Branch(new Branch.NameKey(newProjectNameKey, branch));  db.branches().insert(Collections.singleton(newBranch), txn);  }   - private boolean stringToBoolean(final String boolStr, - final boolean defaultValue) throws Failure { - if (boolStr == null) { - return defaultValue; - } - - if (boolStr.equalsIgnoreCase("FALSE") - || boolStr.equalsIgnoreCase("F") - || boolStr.equalsIgnoreCase("NO") - || boolStr.equalsIgnoreCase("N")) { - return false; - } - - if (boolStr.equalsIgnoreCase("TRUE") - || boolStr.equalsIgnoreCase("T") - || boolStr.equalsIgnoreCase("YES") - || boolStr.equalsIgnoreCase("Y")) { - return true; - } - - throw new Failure(1, "Parameter must have boolean value (true, false)"); - } -  private void validateParameters() throws Failure {  if (projectName.endsWith(".git")) { - projectName = projectName.substring(0, - projectName.length() - ".git".length()); + projectName = + projectName.substring(0, projectName.length() - ".git".length());  }   - if (ownerName == null) { - ownerId = authConfig.getAdministratorsGroup(); - } else { - AccountGroup ownerGroup = groupCache.lookup(ownerName); - if (ownerGroup == null) { - throw new Failure(1, "Specified group does not exist"); - } - ownerId = ownerGroup.getId(); + while (branch.startsWith("/")) { + branch = branch.substring(1);  } - - if (projectDescription == null) { - projectDescription = ""; + if (!branch.startsWith(Constants.R_HEADS)) { + branch = Constants.R_HEADS + branch;  } - - contributorAgreements = stringToBoolean(useContributorAgreements, false); - signedOffBy = stringToBoolean(useSignedOffBy, false); - - if (submitTypeStr == null) { - submitType = SubmitType.FAST_FORWARD_ONLY; - - } else if (submitTypeStr.equalsIgnoreCase("fast-forward-only")) { - submitType = SubmitType.FAST_FORWARD_ONLY; - - } else if (submitTypeStr.equalsIgnoreCase("merge-if-necessary")) { - submitType = SubmitType.MERGE_IF_NECESSARY; - - } else if (submitTypeStr.equalsIgnoreCase("merge-always")) { - submitType = SubmitType.MERGE_ALWAYS; - - } else if (submitTypeStr.equalsIgnoreCase("cherry-pick")) { - submitType = SubmitType.CHERRY_PICK; - - } else { - throw new Failure(1, "Submit type must be either: fast-forward-only, " - + "merge-if-necessary, merge-always or cherry-pick"); + if (!Repository.isValidRefName(branch)) { + throw new Failure(1, "--branch \"" + branch + "\" is not a valid name");  }  }  } -